﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Expanse.Util;
using System.Collections;
using System.Runtime;
using System.Runtime.CompilerServices;

namespace Expanse.Extensions
{
    public static class EnumerableExtensions
    {
        /// <summary>
        /// Returns distinct elements from a sequence using the key selector specfied in <paramref name="keySelector"/>
        /// and the equality comparer specfied in <paramref name="keySelector"/> (or the default equality comparer for 
        /// the key's type if no comparer is specified).
        /// </summary>
        /// <typeparam name="TItem">The type of the elements of <paramref name="seq"/>.</typeparam>
        /// <typeparam name="TCompare">The type of the comparison keys returned by <paramref name="compareKey"/>.</typeparam>
        /// <param name="seq">The sequence to return unique elements from.</param>
        /// <param name="keySelector">The selector expression to be used to generate comparison keys.</param>
        /// <param name="keyComparer">The <see cref="System.Collections.Generic.IEqualityComparer<T>"/> instance to use to
        /// compare the keys, or null if the default equality comparer for the key's type should be used.</param>
        /// <returns>An <see cref="System.Collections.Generic.IEnumerable<T>"/> that contains distinct elements from <paramref name="seq"/>.</returns>
        public static IEnumerable<TItem> DistinctBy<TItem, TCompare>(this IEnumerable<TItem> seq, Func<TItem, TCompare> keySelector, IEqualityComparer<TCompare> keyComparer = null)
        {
            return seq.Distinct(new FunctionalEqualityComparer<TItem, TCompare>(keySelector, keyComparer));
        }

        public static void ForEach<T>(this IEnumerable<T> seq, Action<T> action)
        {
            foreach (T t in seq)
                action(t);
        }

        public static void ForEach<T>(this IEnumerable<T> seq, Action<T, int> action)
        {
            int i = 0;
            foreach (T t in seq)
            {
                action(t, i);
                i++;
            }
        }

        public static void ForEach(this IEnumerable seq, Action<object> action)
        {
            foreach (object t in seq)
                action(t);
        }

        public static void ForEach(this IEnumerable seq, Action<object, int> action)
        {
            int i = 0;
            foreach (object t in seq)
            {
                action(t, i);
                i++;
            }
        }

        public static string JoinAsString<T>(this IEnumerable<T> seq, string separator)
        {
            bool hasSeparator = !string.IsNullOrEmpty(separator);
            var sb = new StringBuilder();
            bool first = true;
            foreach (var s in seq)
            {
                if (first)
                    first = false;
                else if (hasSeparator)
                    sb.Append(separator);
                sb.Append(s);
            }
            return sb.ToString();
        }

        public static string JoinAsString(this IEnumerable<string> seq, string separator)
        {
            bool hasSeparator = !string.IsNullOrEmpty(separator);
            var sb = new StringBuilder();
            bool first = true;
            foreach (var s in seq)
            {
                if (first)
                    first = false;
                else if (hasSeparator)
                    sb.Append(separator);
                sb.Append(s);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Inserts the <paramref name="element"/> specified as a separator between
        /// the elements in <paramref name="seq"/>.
        /// </summary>
        /// <typeparam name="T">The type of the separator and of the elements of <paramref name="seq"/>.</typeparam>
        /// <param name="seq">The sequence to intersperse elements into.</param>
        /// <param name="element">The element to insert as a separator between the elements in <paramref name="seq"/>.</param>
        /// <returns>An <see cref="System.Collections.Generic.IEnumerable<T>"/> containing the elements in <paramref name="seq"/> 
        /// separated by the <paramref name="element"/> specified.</returns>
        public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> seq, T element)
        {
            bool first = true;
            foreach (T value in seq)
            {
                if (!first)
                    first = false;
                else
                    yield return element;
                yield return value;
            }
        }

        /// <summary>
        /// Inserts the elements generated by <paramref name="elementGenerator"/> as separators between
        /// the elements in <paramref name="seq"/>.
        /// </summary>
        /// <typeparam name="T">The type of the separator and of the elements of <paramref name="seq"/>.</typeparam>
        /// <param name="seq">The sequence to intersperse elements into.</param>
        /// <param name="elementGenerator">The function that will be used to generate elements to
        /// insert as separators between the elements in <paramref name="seq"/>.</param>
        /// <returns>An <see cref="System.Collections.Generic.IEnumerable<T>"/> containing the elements in <paramref name="seq"/> 
        /// separated by the elements generated by <paramref name="elementGenerator"/>.</returns>
        public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> seq, Func<T> elementGenerator)
        {
            bool first = true;
            foreach (T value in seq)
            {
                if (!first)
                    first = false;
                else
                    yield return elementGenerator();
                yield return value;
            }
        }

        /// <summary>
        /// Inserts the elements generated by <paramref name="elementGenerator"/> as separators between
        /// the elements in <paramref name="seq"/>.
        /// </summary>
        /// <typeparam name="T">The type of the separator and of the elements of <paramref name="seq"/>.</typeparam>
        /// <param name="seq">The sequence to intersperse elements into.</param>
        /// <param name="elementGenerator">The function that will be used to generate elements to
        /// insert as separators between the elements in <paramref name="seq"/>. The function will receive the element prior to the 
        /// location the generated element will be inserted at.</param>
        /// <returns>An <see cref="System.Collections.Generic.IEnumerable<T>"/> containing the elements in <paramref name="seq"/> 
        /// separated by the elements generated by <paramref name="elementGenerator"/>.</returns>
        public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> seq, Func<T, T> elementGenerator)
        {
            bool first = true;
            foreach (T value in seq)
            {
                if (!first)
                    first = false;
                else
                    yield return elementGenerator(value);
                yield return value;
            }
        }

        /// <summary>
        /// Groups items into same-sized clumps.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source">The source list of items.</param>
        /// <param name="size">The maximum length of each generated clump.</param>
        /// <returns>A list of list of items, where each list of items is no larger than the length given.</returns>
        public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (size < 1)
                throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

            return ClumpIterator<T>(source, size);
        }

        private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
        {
            T[] items = new T[size];
            int count = 0;
            foreach (var item in source)
            {
                items[count] = item;
                count++;

                if (count == size)
                {
                    yield return items;
                    items = new T[size];
                    count = 0;
                }
            }
            if (count > 0)
            {
                if (count == size)
                    yield return items;
                else
                {
                    T[] tempItems = new T[count];
                    Array.Copy(items, tempItems, count);
                    yield return tempItems;
                }
            }
        }

        /// <summary>
        /// Returns the specified sequence, or an empty sequence of the same type
        /// if the specified sequence is a null reference.
        /// </summary>
        /// <typeparam name="T">The type of the elements of <paramref name="seq"/>.</typeparam>
        /// <param name="seq">The sequence to return if it is not null.</param>
        /// <returns>The sequence specified in <paramref name="seq"/> if it is not null -
        /// otherwise an empty sequence of the same element type will be returned.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        [TargetedPatchingOptOut("")]
        public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T> seq)
        {
            return seq ?? Enumerable.Empty<T>();
        }

        /// <summary>
        /// Returns all elements in the specified <paramref name="set"/> that are not also included in the
        /// list of elements specified in <paramref name="exclude"/>.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="set"></param>
        /// <param name="exclude"></param>
        /// <returns></returns>
        public static IEnumerable<T> Except<T>(this IEnumerable<T> set, params T[] exclude)
        {
            return set.Where(i => !exclude.Contains(i));
        }

        #region Windowing

        /// <summary>
        /// Returns a sequence containing a <see cref="Expanse.Util.WindowItem<T>"/> instance for every element
        /// in the source sequence, giving info about the element(s) before and/or after the current element.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="seq">The source sequence.</param>
        /// <param name="before">The number of items previous to the current element to return in the window.</param>
        /// <param name="before">The number of items after the current element to return in the window.</param>
        /// <returns>A sequence containing a <see cref="Expanse.Util.WindowItem<T>"/> instance for every element
        /// in the source sequence, giving info about the element(s) before and/or after the current element.
        /// For more details, see the documentation for <see cref="Expanse.Util.WindowItem<T>"/>. </returns>
        public static IEnumerable<WindowItem<T>> Window<T>(this IEnumerable<T> seq, int before, int after)
        {
            int i = 0;
            int beforeAndAfter = before + after;
            int fullWindowSize = beforeAndAfter + 1;
            T[] allItems = new T[fullWindowSize];

            foreach (var item in seq)
            {
                //have to enumerate at least enough items to populate the full before and after arrays
                if (i >= fullWindowSize - 1)
                {
                    var l = allItems.Length - 1;

                    //shift all items towards beginning by one
                    if (i >= fullWindowSize)
                    {
                        for (var j = 0; j < l; j++)
                            allItems[j] = allItems[j + 1];
                    }

                    //set the last item in the array
                    allItems[l] = item;

                    //window
                    if (i == fullWindowSize - 1) //window for all items before the item at i, and also i
                    {
                        foreach (var it in WindowCore(allItems, 0, 0, before, before, after, fullWindowSize))
                            yield return it;
                    }
                    else //window for a single item
                        yield return WindowCore(allItems, i - beforeAndAfter, before, before, after, fullWindowSize);
                }
                else
                {
                    allItems[i] = item;
                }

                i++;
            }

            //if we never filled the window then we have not output *any* items
            if (i <= fullWindowSize)
            {
                foreach (var item in WindowCore(allItems, 0, 0, i - 1, before, after, i))
                    yield return item;
            }
            //in this case we have output all items up to the last full window, but not the items after that
            else
            {
                foreach (var item in WindowCore(allItems, i - beforeAndAfter - 1, before + 1, allItems.Length - 1, before, after, fullWindowSize))
                    yield return item;
            }
        }


        private static IEnumerable<WindowItem<T>> WindowCore<T>(T[] windowBuffer, int offset, int from, int to, int before, int after, int windowSize)
        {
            for (var i = from; i <= to; i++)
                yield return WindowCore(windowBuffer, offset, i, before, after, windowSize);
        }

        //[MethodImpl(MethodImplOptions.AggressiveInlining)]
        //[TargetedPatchingOptOut("")]
        private static WindowItem<T> WindowCore<T>(T[] windowBuffer, int offset, int at, int before, int after, int windowSize)
        {
            int beforeCount = before < at ? before : at;
            int afterCount = Math.Min((windowSize - at) - 1, after);
            int beforeAndAfter = beforeCount + afterCount;
            var items = new T[beforeAndAfter + 1];
            Array.Copy(windowBuffer, at - beforeCount, items, 0, beforeAndAfter + 1);
            return new WindowItem<T>(offset + at, beforeCount, afterCount, items);
        }

        #endregion

        #region Grouping

        public static IEnumerable<IGrouping<K, T>> GroupBy<T, K>
            (this IAdjacentGrouping<T> source, Func<T, K> keySelector)
        {
            return GroupBy(source, keySelector, null);
        }

        public static IEnumerable<IGrouping<K, T>> GroupBy<T, K>
            (this IAdjacentGrouping<T> source, Func<T, K> keySelector, IEqualityComparer<K> comparer)
        {
            if (comparer == null) comparer = EqualityComparer<K>.Default;

            // Remembers elements of the current group
            List<T> elementsSoFar = new List<T>();
            IEnumerator<T> en = source.GetEnumerator();

            // Read the first element (we need an initial key value)
            if (en.MoveNext())
            {
                K lastKey = keySelector(en.Current);
                do
                {
                    // Test whether current element starts a new group
                    K newKey = keySelector(en.Current);
                    if (!comparer.Equals(newKey, lastKey))
                    {
                        // Yield the previous group and start next one
                        yield return new CustomGrouping<K, T> { Elements = elementsSoFar.ToArray(), Key = lastKey };
                        lastKey = newKey;
                        elementsSoFar.Clear();
                    }

                    // Add element to the current group
                    elementsSoFar.Add(en.Current);
                }
                while (en.MoveNext());

                // Yield the last group of sequence
                yield return new CustomGrouping<K, T> { Elements = elementsSoFar, Key = lastKey };
            }
        }


        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        [TargetedPatchingOptOut("")]
        public static IAdjacentGrouping<T> WithAdjacentGrouping<T>(this IEnumerable<T> e)
        {
            return new WrappedAdjacentGrouping<T> { Wrapped = e };
        }

        #endregion

        #region non-generic IEnumerable
        public static object FirstOrDefault(this IEnumerable enumerable)
        {
            foreach (object obj in enumerable)
                return obj;
            return null;
        }

        #endregion

        /// <summary>
        /// Concatenates the specified <paramref name="enumerables"/> onto the end of the specified
        /// <paramref name="first"/> enumerable, and returns a single enumerable sequence that iterates
        /// over each of them in order.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="first"></param>
        /// <param name="enumerables"></param>
        /// <returns></returns>
        public static IEnumerable<T> ConcatAll<T>(this IEnumerable<T> first, params IEnumerable<T>[] enumerables)
        {
            foreach (var item in first)
                yield return item;

            foreach (var enumerable in enumerables)
                foreach (var item in enumerable)
                    yield return item;
        }

        /// <summary>
        /// Returns whether the sequence is null or has no elements. In order to determine this, the first element
        /// in the sequence will be enumerated if the sequence is not null.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="seq"></param>
        /// <returns></returns>
        public static bool IsNullOrEmpty<T>(this IEnumerable<T> seq)
        {
            return seq == null || !seq.Any();
        }

        /// <summary>
        /// Flattens a sequence of enumerables into a single enumerable instance containing all of the elements
        /// in each of the enumerables in the sequence.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="seq"></param>
        /// <returns></returns>
        public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> seq)
        {
            foreach (var s in seq)
            {
                foreach (var e in s)
                    yield return e;
            }
        }

        public static ulong? Sum(this IEnumerable<ulong?> source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            ulong sum = 0L;
            foreach (ulong? val in source)
            {
                if (val.HasValue)
                    checked { sum += val.GetValueOrDefault(); }
            }
            return (ulong?)sum;
        }

        public static uint? Sum(this IEnumerable<uint?> source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            uint sum = 0;
            foreach (uint? val in source)
            {
                if (val.HasValue)
                    checked { sum += val.GetValueOrDefault(); }
            }
            return (uint?)sum;
        }

        public static ulong Sum(this IEnumerable<ulong> source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            ulong sum = 0L;
            foreach (ulong val in source)
                checked { sum += val; }
            return sum;
        }

        public static uint Sum(this IEnumerable<uint> source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            uint sum = 0;
            foreach (uint val in source)
                checked { sum += val; }
            return sum;
        }

        public static ulong Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, ulong> selector)
        {
            return Sum(Enumerable.Select<TSource, ulong>(source, selector));
        }

        public static ulong? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, ulong?> selector)
        {
            return Sum(Enumerable.Select<TSource, ulong?>(source, selector));
        }

        public static uint Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, uint> selector)
        {
            return Sum(Enumerable.Select<TSource, uint>(source, selector));
        }

        public static ulong? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, uint?> selector)
        {
            return Sum(Enumerable.Select<TSource, uint?>(source, selector));
        }
    }
}
